import Emitter from './emitter';
import ProtoEncoder from './codec/ProtoEncoder';
import ProtoDecoder from './codec/ProtoDecoder';
import { _net_date_decode, _net_date_encode } from './NETGOBERTS';
import LengthOptimizeConfig from './codec/LengthOptimizeConfig';
import NetData from './NetData';


export interface InitProps {
    maxReconnectAttempts: number;//最大尝试重连次数
    reconnect: boolean;//是否重连
}
enum PackageTypes {//this.handlers 使用的处理类型枚举值
    TYPE_DATA = 1,
}
export enum WebSocketClientNetEvent {
    reconnect = "reconnect",
    open = "open",
    error = "error",
    close = "close",
    disconnect = "disconnect",
    reconnectDefeat = "reconnectDefeat",//重连失败
}

export default class WebSocketClient extends Emitter {

    private socket?: WebSocket | null;
    private reqId: number = 0;
    private callbacks: Map<number, Function> = new Map<number, Function>();;
    private handlers: any = {};

    private encoder: ProtoEncoder; // 编码
    private decoder: ProtoDecoder;// 解码工具

    private heartbeatInterval: number = 5000;
    private heartbeatTimeout: number = 10000;//超时剩余时长

    private nextHeartbeatTimeout: number = 0;//心跳超时时间戳
    private gapThreshold: number = 100;   // heartbeat gap threashold
    private heartbeatId?: number | null;
    private heartbeatTimeoutId?: number | null;

    private reconnect: boolean = false;
    private reconncetTimer?: number;
    private reconnectUrl?: string;
    private reconnectAttempts: number = 0;
    private reconnectionDelay: number = 500;
    private DEFAULT_MAX_RECONNECT_ATTEMPTS: number = 3;

    constructor() {
        super()
        this.handlers[PackageTypes.TYPE_DATA] = this.onData.bind(this);
        const len = new LengthOptimizeConfig();
        this.encoder = new ProtoEncoder(len);
        this.decoder = new ProtoDecoder(len);
    }

    public connect(url:string,params?: InitProps) {//初始化ip和端口并连接服务器
        console.log(params);
        if(!params){
            params = {
                maxReconnectAttempts: 3,
                reconnect: true
            }
        }

        this.connect0(params, url)
    }

    private connect0(params: InitProps, url: string = '') {//连接服务器 私有方法不允许外部调用
        if (this.socket) {
            console.log('already connected, return');
            return;
        }
        console.log('connect to ' + url);
        const maxReconnectAttempts = params.maxReconnectAttempts || this.DEFAULT_MAX_RECONNECT_ATTEMPTS;
        const ws = new WebSocket(url)
        this.socket = ws
        ws.binaryType = "arraybuffer";

        ws.onopen = () => {
            if (this.reconnect) {
                this.emit(WebSocketClientNetEvent.reconnect)
                console.log("reconnect")
            }
            this.emit(WebSocketClientNetEvent.open)
            console.log("成功连接服务器");
            this.reset()
            setInterval(this.heartbeat, this.heartbeatInterval);// 目前只做定时 ping 服务器 //TODO
        }

        ws.onmessage = (event: MessageEvent) => {
            this.processPackage(this.decoder.decode(event.data));
            // new package arrived, update the heartbeat timeout
            if (this.heartbeatTimeout) {
                this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout;
            }
        }

        ws.onerror = (event) => {
            this.emit(WebSocketClientNetEvent.error, event);
            console.error('socket error: ', event);
        };

        ws.onclose = (event) => {
            console.log('socket close: ', event);
            this.emit(WebSocketClientNetEvent.close, event);
            this.emit(WebSocketClientNetEvent.disconnect, event);
            if (params.reconnect && this.reconnectAttempts < maxReconnectAttempts) {
                console.log("reconnecting...", params.reconnect, this.reconnectAttempts)
                this.reconnectUrl = url
                this.reconnect = true;
                this.reconnectAttempts++;
                this.reconncetTimer = setTimeout(() => {
                    this.connect0(params, this.reconnectUrl);
                }, this.reconnectionDelay);
                this.reconnectionDelay *= 2;
            } else {
                this.emit(WebSocketClientNetEvent.reconnectDefeat);//重连失败事件触发
            }
        };
    }

    /**
     * 
     * @param route 请求服务器类与方法名，大小写敏感
     * @param msg NetData.createProtoInstance() 创建的proto对象
     * @param cb 回调函数，回调函数的第一个参数为错误信息，第二个参数为服务器返回的NetData.createProtoInstance() 创建的proto对象 TODO
     * @returns 
     */
    public send(route: string, msg: any, cb?: any) {// 发送请求，回调函数
        console.log('send', route, msg);
        if (!route) {
            throw new Error('route is required');
        }
        this.sendMessage({
            message: msg,
            serviceAndMethod: route,
            protoName: msg._protoName,
        }, cb);
    };

    // public notify(route: string, msg: any) {
    //     this.sendMessage({
    //         message: msg,
    //         serviceAndMethod: route,
    //         protoName: msg._protoName,
    //     });
    // };

    public disconnect() {
        const arr = ['disconnect', "close"]
        if (this.socket) {
            // this.socket.close();
            for (let index = 0; index < arr.length; index++) {
                if ((this.socket as any)?.[arr[index]]) {
                    console.log('disconnect fun', arr[index]);
                    (this.socket as any)?.[arr[index]]()
                    break
                }
            }
            this.socket = null;
        }

        if (this.heartbeatId) {
            clearTimeout(this.heartbeatId);
            this.heartbeatId = null;
        }
        if (this.heartbeatTimeoutId) {
            clearTimeout(this.heartbeatTimeoutId);
            this.heartbeatTimeoutId = null;
        }
    };

    private reset() {
        this.reconnect = false;
        this.reconnectionDelay = 5000;
        this.reconnectAttempts = 0;
        clearTimeout(this.reconncetTimer);
    }

    private sendMessage(msg: _net_date_encode, cb?: any) {
        this.reqId++;
        msg.cmdIndex = this.reqId;
        const packet = this.encoder.encode(msg);//编码构建二进制帧数据包
        if (cb)
            this.callbacks[this.reqId] = cb;
        this.send0(packet);//向服务器发送数据包
    };

    private send0(packet: ArrayBuffer) {
        this.socket?.send(packet);
    }
    private processPackage(msgs: _net_date_decode) {
        // console.log('processPackage', msgs);
        this.handlers[PackageTypes.TYPE_DATA](msgs);
    };

    public heartbeat(data?: any) {
        if (!this.heartbeatInterval) {
            return;
        }
        let obj = this.encoder.encode({
            message: NetData.createProtoInstance('EmptyProto'),
            serviceAndMethod: 'accountService.ping',
            protoName: 'EmptyProto',
            cmdIndex: 0
        })
        if (this.heartbeatTimeoutId) {
            // console.log("重置心跳超时", this.heartbeatTimeoutId);
            clearTimeout(this.heartbeatTimeoutId);
            this.heartbeatTimeoutId = undefined;
        }

        if (this.heartbeatId) {
            // already in a heartbeat interval
            return;
        }
        // console.log('发送心跳包倒计时', this.heartbeatInterval);
        this.heartbeatId = setTimeout(() => {

            this.heartbeatId = null;
            this.send0(obj);
            this.nextHeartbeatTimeout = Date.now() + this.heartbeatTimeout;
            this.heartbeatTimeoutId = setTimeout(() => {
                let gap = this.nextHeartbeatTimeout - Date.now();
                if (gap > this.gapThreshold) {
                    this.heartbeatTimeoutId = setTimeout(this.heartbeatTimeoutCb, gap);
                } else {//超时待开发时完善使用 //TODO
                    // console.error('心跳包超时');
                    // this.emit('heartbeat', 'heartbeat timeout');
                    // this.disconnect();

                    // if (this.heartbeatId) {
                    //     console.log('disconnect', this.heartbeatId);
                    //     clearTimeout(this.heartbeatId);
                    //     this.heartbeatId = null;
                    // }
                    // if (this.heartbeatTimeoutId) {
                    //     console.log('disconnect', this.heartbeatTimeoutId);
                    //     clearTimeout(this.heartbeatTimeoutId);
                    //     this.heartbeatTimeoutId = null;
                    // }
                }
            }, this.heartbeatTimeout);
        }, this.heartbeatInterval);
    };

    public heartbeatTimeoutCb() {
        let gap = this.nextHeartbeatTimeout - Date.now();
        if (gap > this.gapThreshold) {
            this.heartbeatTimeoutId = setTimeout(this.heartbeatTimeoutCb, gap);
        } else {//超时待开发时完善使用 //TODO
            // this.emit('heartbeat', 'heartbeat timeout'); 
            // const arr = ['disconnect', "close"]
            // if (this.socket) {
            //     // console.log('disconnect', this.socket);
            //     for (let index = 0; index < arr.length; index++) {
            //         if ((this.socket as any)?.[arr[index]]) {
            //             // console.log('disconnect', arr[index]);
            //             (this.socket as any)?.[arr[index]]()
            //             break
            //         }
            //     }
            //     this.socket = null;
            // }

            // if (this.heartbeatId) {
            //     // console.log('disconnect', this.heartbeatId);
            //     clearTimeout(this.heartbeatId);
            //     this.heartbeatId = null;
            // }
            // if (this.heartbeatTimeoutId) {
            //     // console.log('disconnect', this.heartbeatTimeoutId);
            //     clearTimeout(this.heartbeatTimeoutId);
            //     this.heartbeatTimeoutId = null;
            // }
            // console.error('server heartbeat timeout');
        }
    };

    private onData(data: _net_date_decode) {
        this.processMessage(data);
    };

    private processMessage(msg: _net_date_decode) {
        console.log(msg);
        if (msg.cmdIndex === 0) {
            // server push message
            this.emit(msg.serviceAndMethod, msg.dataProto);
            return;
        }
        //if have a id then find the callback function with the request
        let cb = this.callbacks[msg.cmdIndex];
        // delete cache callback
        this.callbacks.delete(msg.cmdIndex);
        if (typeof cb !== 'function') {
            return;
        }
        cb(msg.dataProto);
        return;
    };

    public isConnected() {
        return this.socket && this.socket.readyState === WebSocket.OPEN;
    }


}